/*
 * Copyright 2013 Pavel Stastny <pavel.stastny at gmail.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.aplikator.server.processes.impl.processes;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.SerializationUtils;
import org.aplikator.client.shared.descriptor.ProcessDTO;
import org.aplikator.server.processes.CannotCallStartException;
import org.aplikator.server.processes.CannotCallStopException;
import org.aplikator.server.processes.ProcessPropeties;
import org.aplikator.server.processes.ProcessState;
import org.aplikator.server.processes.ProcessType;
import org.aplikator.server.processes.RunnableSerializationAware;
import org.aplikator.server.processes.Work;
import org.aplikator.server.processes.impl.ProcessManagerImpl;
import org.aplikator.server.util.IOUtils;
import org.json.JSONException;
import org.json.JSONObject;

/**
 *
 * @author Pavel Stastny <pavel.stastny at gmail.com>
 */
public abstract class AbstractOSProcess implements org.aplikator.server.processes.Process{

    private static final Logger LOGGER = Logger.getLogger(AbstractOSProcess.class.getName());

    
    /** Process identifier */
    private String identifier;

    /** Runnable part, may be null */
    private RunnableSerializationAware runnable;

    /** Works, may be null */
    private Work[] works;
    
    /** Redirected stdout */
    private File stdoutFile;
    
    /** Redirected sterr */
    private File sterrFile;
    
    private File userDir;
    
    private transient Process process;
    private ProcessState processState = ProcessState.CREATED;
    
    private String[] classpath = new String[0];
    private String[] libs = new String[0];
    
    private String appname;

    private int pid;
    
    private Date startedDate = null;
    private Date stoppedDate = null;
    
    private ProcessPropeties processProperties = new ProcessPropeties();
    
    public AbstractOSProcess(String identifier, String appname, RunnableSerializationAware runnable, String[] libs, String[] classpath) {
        this.identifier = identifier;
        this.runnable = runnable;
        this.appname = appname;
        this.libs = (libs != null) ? libs : new String[] {} ;
        this.classpath = (classpath != null) ? classpath : new String[] {};
    }

    public AbstractOSProcess(String identifier, String appname, Work[] works, String[] libs, String[] classpath) {
        this.identifier = identifier;
        this.works = works;
        this.appname = appname;
        this.libs = (libs != null) ? libs : new String[] {} ;
        this.classpath = (classpath != null) ? classpath : new String[] {};
    }

    
    
    @Override
    public String getIdentifier() {
        return this.identifier;
    }

    protected abstract void stopMeOSDependent() throws CannotCallStopException;
    
    @Override
    public void stopMe() throws CannotCallStopException {
    	this.stopMeOSDependent();
    	this.processState = ProcessState.KILLED;
    	this.stoppedDate = new Date();
    }

    
    @Override
    public boolean isLiveProcess() {
        try {
            this.process.exitValue();
            return true;
        }catch(Exception ex) {
            return false;
        }
    }

    @Override
    @SuppressWarnings("empty-statement")
    public void startMe() throws CannotCallStartException {
        try {
            
            StringBuilder buffer = new StringBuilder();
            buffer.append(createClasspath(this.classpath));

            for (String libFolder : libs) {
                File f = new File(libFolder);
                File[] files = f.listFiles();
                if (files != null) {
                    String[] items = new String[files.length];
                    for (int i = 0; i < files.length; i++) { items[i] = files[i].getAbsolutePath(); }
                    buffer.append(createClasspath(items));
                }
            }
            
            
            List<String> command = new ArrayList<String>();
            command.add("java");
            
            // works
            if (this.works != null) {
                StringBuilder builder = new StringBuilder();
                for (int i = 0; i < this.works.length; i++) {
                    File tmpFile = File.createTempFile("work", getIdentifier());
                    byte[] serialized = SerializationUtils.serialize(this.works[i]);
                    IOUtils.saveToFile(serialized, tmpFile);
                    if (i > 0) builder.append(File.pathSeparator);
                    builder.append(tmpFile.getAbsolutePath());
                }
                command.add("-D"+ProcessStarter.WORKS_STREAM_FILES+"="+builder.toString());
            } else if (runnable != null) {
                File tmpFile = File.createTempFile("work", getIdentifier());
                byte[] serialized = SerializationUtils.serialize(this.runnable);
                IOUtils.saveToFile(serialized, tmpFile);
                LOGGER.info("serialized to file :"+tmpFile.getAbsolutePath());
                command.add("-D"+ProcessStarter.RUNNABLE_STREAM_FILE+"="+tmpFile.getAbsolutePath());
            }
            
            
            
            if (this.stdoutFile != null) {
                command.add("-D"+ProcessStarter.SOUT_FILE+"="+this.stdoutFile.getAbsolutePath());
            }
            
            if (this.sterrFile != null) {
                command.add("-D"+ProcessStarter.SERR_FILE+"="+this.sterrFile.getAbsolutePath());
            }

            command.add("-D"+ProcessStarter.UUID+"="+this.identifier);
            command.add("-D"+ProcessStarter.APP_NAME+"="+this.appname);

            command.add(ProcessStarter.class.getName());
            LOGGER.info(command.toString());
            
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            processBuilder.environment().put(ProcessStarter.CLASSPATH, buffer.toString());
            //processBuilder = processBuilder.directory(getUserDir());
            LOGGER.info(processBuilder.command().toString());
            this.process = processBuilder.start();
            LOGGER.info("process "+this.process);

            this.startedDate = new Date();
            LOGGER.info("process properties "+this.processProperties);
            
            
        } catch (IOException iOException) {
            LOGGER.log(Level.SEVERE,iOException.getMessage(), iOException);
            this.stoppedDate = new Date();
            throw new CannotCallStartException(iOException);
        } catch(Throwable t) {
            LOGGER.log(Level.SEVERE,t.getMessage(), t);
            this.stoppedDate = new Date();
            throw new CannotCallStartException(t);
        }
    }

    @Override
    public void redirectOutputStream(File f) {
        this.stdoutFile = f;
    }

    @Override
    public void redirectErrorStream(File f) {
        this.sterrFile = f;
    }

    


    @Override
    public ProcessType getType() {
        return ProcessType.PROCESS;
    }

    @Override
    public ProcessState getProcessState() {
        return this.processState;
    }

    public void setProcessState(ProcessState st) {
        this.processState = st;
    }
 
    @Override
    public String getApplicationName() {
        return this.appname;
    }

    public int getPid() {
        return pid;
    }

    public void setPid(int pid) {
        this.pid = pid;
    }

    @Override
    public ProcessPropeties getProcessProperties() {
        return this.processProperties;
    }

    @Override
    public String toString() {
        return "AbstractOSProcess{" + "identifier=" + identifier + ", stdoutFile=" + stdoutFile + ", sterrFile=" + sterrFile + ", userDir=" + userDir + ", processState=" + processState + ", appname=" + appname + ", pid=" + pid + '}';
    }
    
    private String createClasspath(String[] classpathitems) {
        StringBuilder buffer = new StringBuilder();
        for (String cpitem : classpathitems) {
                buffer.append(cpitem);
                buffer.append(File.pathSeparator);
        }
        return buffer.toString();
    }

    @Override
    public void updateProcessIntrnalState(Object value) {
        if (value instanceof JSONObject) {
            JSONObject jsonObject = (JSONObject) value;
            try {
                String actString = jsonObject.getString("action");
                UpdateAction act = UpdateAction.valueOf(actString);
                if (act != null) act.perform(this, jsonObject);
            } catch (JSONException ex) {
                Logger.getLogger(AbstractOSProcess.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else throw new UnsupportedOperationException("unsupported "); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public ProcessDTO toProcessDTO() {
        ProcessDTO pdto = new ProcessDTO();
        if (this.startedDate != null) pdto.setStartedDate(FORMAT.format(this.startedDate));
        if (this.stoppedDate != null) pdto.setStoppedDate(FORMAT.format(this.stoppedDate));
        
        pdto.setIdentifier(this.identifier);
        pdto.setProcessType(this.getType().name());
        pdto.setLiveProcess(this.isLiveProcess());
        pdto.setProcessState(getProcessState().name());
        pdto.setProcessProperties(getProcessProperties().toMap());
        
        return pdto;
    }

    
    
    
    @Override
	public Date getStartedDate() {
    	return this.startedDate;
    }

	@Override
	public Date getStoppedDate() {
		return this.stoppedDate;
	}




	static enum UpdateAction {
        

        updateState {
            
            public void perform(AbstractOSProcess process, JSONObject obj) {
                try {
                    String str = obj.getString("value");
                    ProcessState state = ProcessState.valueOf(str);
                    process.setProcessState(state);
                } catch (JSONException ex) {
                    Logger.getLogger(ProcessManagerImpl.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        },
            
        updatePid {
            public void perform(AbstractOSProcess process, JSONObject obj) {
                try {
                    int pid = obj.getInt("value");
                    process.setPid(pid);
                    System.out.println("UPDATING PID = "+pid);
                } catch (JSONException ex) {
                    Logger.getLogger(ProcessManagerImpl.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        };
        
        public abstract void perform(AbstractOSProcess process, JSONObject obj);
    }


    
}
